/* 
 * Simple syntax parser with C++ comments
 * 
 * Copyright (c) Sergey B. Berezin (s_berezin@cs.msu.su), 2002-2006.
 *
 * You may freely distribute or modify this source code retaining the 
 * above copyright notice.
 *
 * This software is provided "as-is" and without guarantee or warrantee 
 * of any kind, expressed or implied :)
 */

using System;
using System.IO;
using System.Text;
using System.Diagnostics;
using System.Collections.Generic;
using System.Globalization;
using Faml.Frontend;

namespace Faml.Frontend
{

    [Flags]
    public enum ParserMode
    {
        SignedNumbers = 0x00000001,
        SingleLineComments = 0x00000002,
        MultiLineComments = 0x00000004
    }

    public enum TokenType
    {
        /// <summary>Empty token</summary>
        Nothing = -1,
        /// <summary>String in single quotes</summary>
        String1,
        /// <summary>String in double quotes</summary>
        String2,
        /// <summary>Identifier</summary>
        Ident,
        /// <summary>Integer constant</summary>
        Integer,
        /// <summary>Floating point constant with double precision</summary>
        Double,
        /// <summary>DateTime</summary>
        DateTime,
        /// <summary>Time interval</summary>
        TimeSpan,
        /// <summary>Delimiters (commas,braces,semicolons, etc...)</summary>
        Delimiter,
        /// <summary>Operators (plus, minus, equality, inequality, lambda)</summary>
        Operator,
        /// <summary>End of input</summary>
        Eof
    }

    public class ParserException : FamlFrontentException
    {
        public ParserException(Location loc, string message)
            : base(loc, message)
        { /* Nothing to do here */ }
        public ParserException(string message)
            : base(message)
        { /* Nothing to do here */ }
        public ParserException()
            : base()
        { /* Nothing to do here */ }

        public static void Throw(string message)
        {
            throw new ParserException(message);
        }

        public static void Throw(Location loc, string message)
        {
            throw new ParserException(loc, message);
        }

        public static void Throw(Parser p, string message)
        {
            Throw(p.CurrentToken.Location, message);
        }
    }

    public class Token
    {
        /// <summary>Location of this token in source</summary>
        private Location location;
        private string text;
        private int integerValue;
        private double doubleValue;
        private DateTime dateTime;
        private TimeSpan timeSpan;
        private TokenType type;

        public Token(int i, Location loc)
        {
            text = i.ToString();
            integerValue = i;
            doubleValue = 0;
            type = TokenType.Integer;
            location = loc;
        }

        public Token(double d, Location loc)
        {
            text = d.ToString(CultureInfo.InvariantCulture);
            integerValue = 0;
            doubleValue = d;
            type = TokenType.Double;
            location = loc;
        }

        public Token(DateTime dateTime, Location loc)
        {
            text = dateTime.ToString();
            integerValue = unchecked((int)dateTime.Ticks);
            doubleValue = dateTime.Ticks;
            this.dateTime = dateTime;
            type = TokenType.DateTime;
            location = loc;
        }

        public Token(TimeSpan timeSpan, Location loc)
        {
            text = timeSpan.ToString();
            integerValue = unchecked((int)timeSpan.Ticks);
            doubleValue = timeSpan.Ticks;
            this.timeSpan = timeSpan;
            type = TokenType.TimeSpan;
            location = loc;
        }

        public Token(TokenType type, string text, Location loc)
        {
            this.text = text;
            this.type = type;
            integerValue = 0;
            doubleValue = 0;
            location = loc;
        }

        public Token(TokenType type, char ch, Location loc)
        {
            text = ch.ToString();
            this.type = type;
            integerValue = 0;
            doubleValue = 0;
            location = loc;
        }

        public Location Location
        {
            get { return location; }
        }

        public string Text
        {
            get { return text; }
        }

        public int IntegerValue
        {
            get { return integerValue; }
        }

        public double DoubleValue
        {
            get { return doubleValue; }
        }

        public DateTime DateTime
        {
            get { return dateTime; }
        }

        public TimeSpan TimeSpan
        {
            get { return timeSpan; }
        }

        public TokenType Type
        {
            get { return type; }
        }
    }

    /// <summary>
    /// Parses a textual expression to a sequence of tokens
    /// </summary>
    public class Parser
    {
        protected ParserInput reader = null;
        protected ParserMode mode = /*ParserMode.SignedNumbers |*/
                                    ParserMode.SingleLineComments |
                                    ParserMode.MultiLineComments;
        protected Stack<Token> tokens = new Stack<Token>();

        public Parser(ParserInput r)
        {
            reader = r;
        }

        public Parser(TextReader r)
        {
            reader = new TextReaderParserInput(r);
        }

        public Parser(Stream s)
        {
            reader = new StreamParserInput(s);
        }

        public Parser(string s)
        {
            reader = new StringParserInput(s);
        }

        public Token CurrentToken
        {
            get
            {
                return tokens.Peek();
            }
        }

        public string TokenText
        {
            get
            {
                return CurrentToken.Text;
            }
        }

        public TokenType TokenType
        {
            get
            {
                return CurrentToken.Type;
            }
        }

        public int IntegerValue
        {
            get
            {
                return CurrentToken.IntegerValue;
            }
        }

        public double DoubleValue
        {
            get
            {
                return CurrentToken.DoubleValue;
            }
        }

        public int GetPositiveInteger()
        {
            if (MoveNext() != TokenType.Integer || CurrentToken.IntegerValue <= 0)
                ParserException.Throw(this, "Expecting positive int");
            return CurrentToken.IntegerValue;
        }

        public double GetDouble()
        {
            if (MoveNext() == TokenType.Double)
                return DoubleValue;
            else if (TokenType == TokenType.Integer)
                return IntegerValue;
            else
                ParserException.Throw(this, "Expecting floating-point number");
            return 0;
        }

        /// <summary>
        /// Moves to next token
        /// </summary>
        /// <returns>Type of the token</returns>
        public TokenType MoveNext()
        {   // Parser is not initialized - push first token
            if (tokens.Count == 0)
                tokens.Push(GetNextToken());
            else
            {
                // We are at the end of stream
                if (tokens.Count > 0 && CurrentToken.Type == TokenType.Eof)
                    return TokenType.Eof;

                // Remove current token from stack
                tokens.Pop();

                // Get next token if stack is empty
                if (tokens.Count == 0)
                    tokens.Push(GetNextToken());
            }
            // Return current token's type
            return CurrentToken.Type;
        }

        /// <summary>Puts token back to the parses state. This token becames current</summary>
        /// <param name="token">Token to put to parser state</param>
        public void PutBack(Token token)
        {
            tokens.Push(token);
        }

        protected Token GetNextToken()
        {

            // Skip whitespaces and comments
            int ch = GetChar();
            while (ch != -1 && Char.IsWhiteSpace((char)ch))
                ch = GetChar();

            if (ch == -1)
                return new Token(TokenType.Eof, "", reader.CurrentLocation);
            // Plus or minus
            if (ch == '+' || ch == '-')
            {
                if ((mode & ParserMode.SignedNumbers) != 0)
                {
                    reader.PutBack((char)ch);
                    return GetNumber();
                }
                else
                    return new Token(TokenType.Operator, (char)ch, reader.CurrentLocation);
            }
            else if (Char.IsDigit((char)ch))
            {
                reader.PutBack((char)ch);
                return GetNumber();
            }
            else if (ch == '.')
            {
                int ch2 = reader.GetChar();
                reader.PutBack((char)ch2);
                if (Char.IsDigit((char)ch2))
                {
                    reader.PutBack((char)ch);
                    return GetNumber();
                }
                else
                    return new Token(TokenType.Delimiter, (char)ch, reader.CurrentLocation);
            }
            else if (Char.IsLetter((char)ch) || ch == '_')
            {
                reader.PutBack((char)ch);
                return GetIdent();
            }
            else if ((int)ch == '@') // DateTime: @"2001-01-09 20:12:02"
            {
                return GetDateTime();
            }
            else if ((int)ch == '=')
            {
                int ch2 = reader.GetChar();
                if ((char)ch2 == '>')
                    return new Token(TokenType.Operator, "=>", reader.CurrentLocation); // Lambda operator
                else if ((char)ch2 == '=')
                    return new Token(TokenType.Operator, "==", reader.CurrentLocation); // Equality operator
                else
                {
                    reader.PutBack((char)ch2);
                    return new Token(TokenType.Delimiter, (char)ch, reader.CurrentLocation);
                }
            }
            else if ((int)ch == '>')
            {
                int ch2 = reader.GetChar();
                if ((char)ch2 == '=')
                    return new Token(TokenType.Operator, ">=", reader.CurrentLocation); // GE operator
                else
                {
                    reader.PutBack((char)ch2);
                    return new Token(TokenType.Operator, (char)ch, reader.CurrentLocation); // GT
                }
            }
            else if ((int)ch == '<')
            {
                int ch2 = reader.GetChar();
                if ((char)ch2 == '=')
                    return new Token(TokenType.Operator, "<=", reader.CurrentLocation); // LE operator
                else
                {
                    reader.PutBack((char)ch2);
                    return new Token(TokenType.Operator, (char)ch, reader.CurrentLocation); // LT
                }
            }
            else if ((int)ch == '&')
            {
                int ch2 = reader.GetChar();
                if ((char)ch2 == '&')
                    return new Token(TokenType.Operator, "&&", reader.CurrentLocation); // AND operator
                else
                {
                    reader.PutBack((char)ch2);
                    ParserException.Throw(this, "unknown token '&'");
                    return null;
                }
            }
            else if ((int)ch == '|')
            {
                int ch2 = reader.GetChar();
                if ((char)ch2 == '|')
                    return new Token(TokenType.Operator, "||", reader.CurrentLocation); // OR operator
                else
                {
                    reader.PutBack((char)ch2);
                    ParserException.Throw(this, "unknown token '|'");
                    //return new Token(TokenType.Delimiter, (char)ch);
                }
            }
            else if (ch == '\"')
            {
                return GetString(ch);
            }
            else
                return new Token(TokenType.Delimiter, (char)ch, reader.CurrentLocation);
            throw new ParserException(reader.CurrentLocation, "Unknown token");
        }


        private Token GetDateTime()
        {
            StringBuilder buffer = new StringBuilder();

            int ch = GetChar();
            if (ch != '\"')
                ParserException.Throw("expect for '\"' after '@' (a date string)");
            ch = GetChar();
            while (ch != '\"')
            {
                buffer.Append((char)ch);
                ch = GetChar();
            }

            string text = buffer.ToString();

            DateTime dateTime;
            if (!DateTime.TryParse(text, out dateTime))
            {
                string[] items = text.Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
                if (items.Length == 0)
                    ParserException.Throw("time interval is empty");

                int days = -1, hours = -1, mins = -1, secs = -1, ms = -1;
                foreach (string item in items)
                {
                    char suffix = item[item.Length - 1];
                    switch (char.ToLower(suffix))
                    {
                        case 'd':
                            if (days != -1)
                                ParserException.Throw(this, "time interval has duplicate entries");
                            days = int.Parse(item.Substring(0, item.Length - 1));
                            break;
                        case 'h':
                            if (hours != -1)
                                ParserException.Throw(this, "time interval has duplicate entries");
                            hours = int.Parse(item.Substring(0, item.Length - 1));
                            break;
                        case 'm':
                            if (mins != -1)
                                ParserException.Throw(this, "time interval has duplicate entries");
                            mins = int.Parse(item.Substring(0, item.Length - 1));
                            break;
                        case 's':
                            if (secs != -1)
                                ParserException.Throw(this, "time interval has duplicate entries");
                            secs = int.Parse(item.Substring(0, item.Length - 1));
                            break;
                        case 'w':
                            if (ms != -1)
                                ParserException.Throw(this, "time interval has duplicate entries");
                            ms = int.Parse(item.Substring(0, item.Length - 1));
                            break;

                        default:
                            throw new ParserException(reader.CurrentLocation, "time interval has unknown entry");
                    }
                }

                return new Token(new TimeSpan((days > 0) ? days : 0,
                                         (hours > 0) ? hours : 0,
                                         (mins > 0) ? mins : 0,
                                         (secs > 0) ? secs : 0,
                                         (ms > 0) ? ms : 0), reader.CurrentLocation);
            }

            return new Token(dateTime, reader.CurrentLocation);
        }

        protected Token GetIdent()
        {
            StringBuilder sb = new StringBuilder();
            int ch = GetChar();
            while (ch != -1 && (Char.IsLetter((char)ch) || Char.IsDigit((char)ch) || ((char)ch) == '_')
                || (((char)ch) == ':'))
            {
                sb.Append((char)ch);
                ch = GetChar();
            }
            if (ch != -1)
                reader.PutBack((char)ch);
            return new Token(TokenType.Ident, sb.ToString(), reader.CurrentLocation);
        }

        protected Token GetString(int quote)
        {
            StringBuilder buffer = new StringBuilder();

            int ch = GetChar();
            while (ch != quote)
            {
                buffer.Append((char)ch);
                ch = GetChar();
            }

            string text = buffer.ToString();

            if (quote == '\"')
                return new Token(TokenType.String2, text, reader.CurrentLocation);
            else
                return new Token(TokenType.String1, text, reader.CurrentLocation);
        }

        protected Token GetNumber()
        {
            StringBuilder buffer = new StringBuilder();
            bool floatingPoint = false;
            int ch = GetChar();
            if (ch == '+' || ch == '-')
            {
                buffer.Append((char)ch);
                ch = GetChar();
            }
            while (Char.IsDigit((char)ch))
            {
                buffer.Append((char)ch);
                ch = GetChar();
            }
            if (ch == '.')
            {
                floatingPoint = true;
                buffer.Append((char)ch);
                ch = GetChar();
                while (Char.IsDigit((char)ch))
                {
                    buffer.Append((char)ch);
                    ch = GetChar();
                }
            }
            if (ch == 'e' || ch == 'E')
            {
                floatingPoint = true;
                buffer.Append((char)ch);
                ch = GetChar();
                if (ch == '+' || ch == '-')
                {
                    buffer.Append((char)ch);
                    ch = GetChar();
                }
                if (!Char.IsDigit((char)ch))
                    ParserException.Throw(this, "the number has an incorrect syntax");
                while (Char.IsDigit((char)ch))
                {
                    buffer.Append((char)ch);
                    ch = GetChar();
                }
            }
            reader.PutBack((char)ch);
            string tokenText = buffer.ToString();
            System.Globalization.NumberFormatInfo fmt = new System.Globalization.NumberFormatInfo();
            fmt.NumberDecimalSeparator = ".";
            try
            {
                if (floatingPoint)
                    return new Token(Double.Parse(tokenText, fmt), reader.CurrentLocation);
                else
                    return new Token(Int32.Parse(tokenText, fmt), reader.CurrentLocation);
            }
            catch (Exception)
            {
                throw new ParserException(reader.CurrentLocation, "the number has an incorrect syntax");
            }
        }

        public bool MatchDelimiter(string ch)
        {
            return (CurrentToken.Type == TokenType.Delimiter && CurrentToken.Text == ch);
        }

        public bool MatchOperator(string op)
        {
            return (CurrentToken.Type == TokenType.Operator && CurrentToken.Text == op);
        }

        protected int GetChar()
        {
            int ch = reader.GetChar();
            if (ch == -1) // EOF
                return -1;
            if (ch == '/')
            { // Possible start of a comment
                int ch2 = reader.GetChar();
                if (ch2 == '/' && (mode & ParserMode.SingleLineComments) != 0)
                {
                    while ((ch = reader.GetChar()) != '\n' && ch != -1)
                        ;
                }
                else if (ch2 == '*' && (mode & ParserMode.MultiLineComments) != 0)
                {
                    int count = 1; // allow nested comments
                    while (true)
                    {
                        if ((ch = reader.GetChar()) == -1) // EOF in comment
                            ParserException.Throw(this, "EOF in comment");
                        else if (ch == '*')
                        {
                            if ((ch2 = reader.GetChar()) == -1) // EOF in comment
                                ParserException.Throw(this, "EOF in comment");
                            else if (ch2 == '/')
                                if (--count <= 0)
                                {
                                    ch = ' ';
                                    break;
                                }
                        }
                        else if (ch == '/')
                        {
                            if ((ch2 = reader.GetChar()) == -1) // EOF in comment
                                ParserException.Throw(this, "EOF in comment");
                            else if (ch2 == '*')
                                count++;
                        }
                    }
                }
                else   // Not a comment
                    reader.PutBack((char)ch2);
            }
            return ch;
        }

        public bool MatchKeyword(string text)
        {
            return (TokenType == TokenType.Ident && TokenText == text);
        }
    }
}